package cyou.breathe.service.impl;

import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import cyou.breathe.DistributedCache;
import cyou.breathe.chain.AbstractChainContext;
import cyou.breathe.core.UserContext;
import cyou.breathe.core.UserInfoDTO;
import cyou.breathe.dto.req.UserDeletionReqDTO;
import cyou.breathe.dto.req.UserLoginReqDTO;
import cyou.breathe.dto.req.UserRegisterReqDTO;
import cyou.breathe.dto.resp.UserLoginRespDTO;
import cyou.breathe.dto.resp.UserQueryRespDTO;
import cyou.breathe.dto.resp.UserRegisterRespDTO;
import cyou.breathe.entity.*;
import cyou.breathe.enums.UserChainMarkEnum;
import cyou.breathe.exception.ClientException;
import cyou.breathe.exception.ServiceException;
import cyou.breathe.mapper.*;
import cyou.breathe.service.UserLoginService;
import cyou.breathe.service.UserService;
import cyou.breathe.tools.BeanUtils;
import cyou.breathe.tools.JWTUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import static cyou.breathe.constant.RedisKeyConstant.*;
import static cyou.breathe.enums.UserRegisterErrorCodeEnum.*;
import static cyou.breathe.tools.UserReuseUtils.hashShardingIdx;

/**
 * @author: breathe
 * @createTime: 2025-06-16
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class UserLoginServiceImpl implements UserLoginService {
    private final UserMailMapper userMailMapper;
    private final UserPhoneMapper userPhoneMapper;
    private final UserMapper userMapper;
    private final UserDeletionMapper userDeletionMapper;
    private final UserService userService;
    private final DistributedCache distributedCache;
    private final AbstractChainContext<UserRegisterReqDTO> abstractChainContext;
    private final RBloomFilter<String> userRegisterCachePenetrationBloomFilter;
    private final RedissonClient redissonClient;
    private final UserReuseMapper userReuseMapper;

    /**
     * 不知道用户用的什么登录，所以第一步先获取用户名
     * 用户名获取成功以后，在用户表中通过用户名和密码登录，密码的加密逻辑直接用shardingsphere实现了
     * 获取用户信息以后，再进行UserInfo的构建，然后获取jwt令牌，jwt作为key，userInfo作为value放到redis中
     * 虽然在bizs中，不是在redis中获取用户登录信息，而是在request的Header中，但是这个redis还可以用来校验用户是否登录，主要作用是用来管理jwt的生命周期
     */
    @Override
    public UserLoginRespDTO login(UserLoginReqDTO requestParam) {
        String usernameOrMailOrPhone = requestParam.getUsernameOrMailOrPhone();
        boolean mailFlag = false;
        for (char c : usernameOrMailOrPhone.toCharArray()) {
            if (c == '@') {
                mailFlag = true;
                break;
            }
        }
        String username;
        if (mailFlag) {
            username = resolveUsername(
                    usernameOrMailOrPhone,
                    val -> Wrappers.lambdaQuery(UserMailDO.class).eq(UserMailDO::getMail, val),
                    UserMailDO::getUsername,
                    userMailMapper::selectOne
            );
        } else {
            username = resolveUsername(
                    usernameOrMailOrPhone,
                    val -> Wrappers.lambdaQuery(UserPhoneDO.class).eq(UserPhoneDO::getPhone, val),
                    UserPhoneDO::getUsername,
                    userPhoneMapper::selectOne
            );
        }
        username = Optional.ofNullable(username)
                .orElse(requestParam.getUsernameOrMailOrPhone());
        LambdaQueryWrapper<UserDO> queryWrapper = Wrappers.lambdaQuery(UserDO.class)
                .eq(UserDO::getUsername, username)
                .eq(UserDO::getPassword, requestParam.getPassword())
                .select(UserDO::getId, UserDO::getUsername, UserDO::getRealName);
        UserDO userDO = userMapper.selectOne(queryWrapper);
        if (userDO == null) {
            throw new ServiceException("账号不存在或密码错误");
        }
        UserInfoDTO userInfo = UserInfoDTO.builder()
                .userId(String.valueOf(userDO.getId()))
                .username(userDO.getUsername())
                .realName(userDO.getRealName())
                .build();
        String accessToken = JWTUtils.generateAccessToken(userInfo);
        UserLoginRespDTO actual = new UserLoginRespDTO(userInfo.getUserId(), requestParam.getUsernameOrMailOrPhone(), userDO.getRealName(), accessToken);
        distributedCache.put(accessToken, JSON.toJSONString(actual), 30, TimeUnit.MINUTES);
        return actual;
    }

    /**
     * 先判断参数，然后通过用户名来锁，锁的粒度是用户名
     * 然后尝试锁，没成功的话就是两个人并发注册，直接让慢的那个抛出异常
     * 先尝试插入user表
     * 再尝试插入两个路由表
     * 再将reuse表中的名字delete掉
     * 最后从set中移除掉
     * 添加到布隆过滤器中
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public UserRegisterRespDTO register(UserRegisterReqDTO requestParam) {
        abstractChainContext.handler(UserChainMarkEnum.USER_REGISTER_FILTER.name(), requestParam);
        RLock lock = redissonClient.getLock(LOCK_USER_REGISTER + requestParam.getUsername());
        boolean tryLock = lock.tryLock();
        if (!tryLock) {
            throw new ServiceException(HAS_USERNAME_NOTNULL);
        }
        try {
            try {
                int inserted = userMapper.insert(BeanUtils.convert(requestParam, UserDO.class));
                if (inserted < 1) {
                    throw new ServiceException(USER_REGISTER_FAIL);
                }
            } catch (DuplicateKeyException dke) {
                log.error("用户名 [{}] 重复注册", requestParam.getUsername());
                throw new ServiceException(HAS_USERNAME_NOTNULL);
            }
            UserPhoneDO userPhoneDO = UserPhoneDO.builder()
                    .phone(requestParam.getPhone())
                    .username(requestParam.getUsername())
                    .build();
            try {
                userPhoneMapper.insert(userPhoneDO);
            } catch (DuplicateKeyException dke) {
                log.error("用户 [{}] 注册手机号 [{}] 重复", requestParam.getUsername(), requestParam.getPhone());
                throw new ServiceException(PHONE_REGISTERED);
            }
            if (StrUtil.isNotBlank(requestParam.getMail())) {
                UserMailDO userMailDO = UserMailDO.builder()
                        .mail(requestParam.getMail())
                        .username(requestParam.getUsername())
                        .build();
                try {
                    userMailMapper.insert(userMailDO);
                } catch (DuplicateKeyException dke) {
                    log.error("用户 [{}] 注册邮箱 [{}] 重复", requestParam.getUsername(), requestParam.getMail());
                    throw new ServiceException(MAIL_REGISTERED);
                }
                String username = requestParam.getUsername();
                userReuseMapper.delete(Wrappers.update(new UserReuseDO(username)));
                StringRedisTemplate instance = (StringRedisTemplate) distributedCache.getInstance();
                instance.opsForSet().remove(USER_REGISTER_REUSE_SHARDING + hashShardingIdx(username), username);
                userRegisterCachePenetrationBloomFilter.add(username);
            }
        } finally {
            lock.unlock();
        }
        return BeanUtils.convert(requestParam, UserRegisterRespDTO.class);
    }

    @Override
    public boolean hasUsername(String username) {
        boolean hasUsername = userRegisterCachePenetrationBloomFilter.contains(username);
        if (hasUsername) {
            StringRedisTemplate instance = (StringRedisTemplate) distributedCache.getInstance();
            //如果存在的话，在分片的set集合中查询是否存在，若存在就 说明该用户名可以使用
            return instance.opsForSet().isMember(USER_REGISTER_REUSE_SHARDING + hashShardingIdx(username), username);
        }
        return true;
    }

    /**
     * 通过查询redis来管理accessToken的生命周期
     */
    @Override
    public UserLoginRespDTO checkLogin(String accessToken) {
        return distributedCache.get(accessToken, UserLoginRespDTO.class);
    }

    /**
     * 直接删除redis的accessToken，登录状态还得是通过redis来管理
     * 到时候这里的判断是和网关结合的。
     */
    @Override
    public void logout(String accessToken) {
        if (StrUtil.isNotBlank(accessToken)) {
            distributedCache.delete(accessToken);
        }
    }

    /**
     * 是通过用户名来注销用户，所以用户名和UserContext中的做一个校验
     * 然后加锁防并发，粒度是用户名
     * 往用户删除表中插入用户名
     * 删除两个路由表中的用户记录
     * 删除redis中的token
     * 往用户名复用表中插入数据
     * 然后往redis的set集合中插入该用户名
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void deletion(UserDeletionReqDTO requestParam) {
        String username = UserContext.getUsername();
        if (!Objects.equals(username, requestParam.getUsername())) {
            //此处严谨来说，需要上报风控系统进行异常检测，因为这个人的操作是利用自己的账号去注销别人的账号，这肯定是不对的。要进行封禁等处理
             throw new ClientException("注销账号和登录账号不一致");
        }
        RLock lock = redissonClient.getLock(USER_DELETION + requestParam.getUsername());
        lock.lock();
        try {
            UserQueryRespDTO userQueryRespDTO = userService.queryUserByUsername(username);
            UserDeletionDO userDeletionDO = UserDeletionDO.builder()
                    .idType(userQueryRespDTO.getIdType())
                    .idCard(userQueryRespDTO.getIdCard())
                    .build();
            userDeletionMapper.insert(userDeletionDO);
            UserDO userDO = new UserDO();
            userDO.setDeletionTime(System.currentTimeMillis());
            userDO.setUsername(username);
            userMapper.deletionUser(userDO);
            UserPhoneDO userPhoneDO = UserPhoneDO.builder()
                    .phone(userQueryRespDTO.getPhone())
                    .deletionTime(System.currentTimeMillis())
                    .build();
            userPhoneMapper.deletionUser(userPhoneDO);
            if (StrUtil.isNotBlank(userQueryRespDTO.getMail())) {
                UserMailDO userMailDO = UserMailDO.builder()
                        .mail(userQueryRespDTO.getMail())
                        .deletionTime(System.currentTimeMillis())
                        .build();
                userMailMapper.deletionUser(userMailDO);
            }
            distributedCache.delete(UserContext.getToken());
            userReuseMapper.insert(new UserReuseDO(username));
            StringRedisTemplate instance = (StringRedisTemplate) distributedCache.getInstance();
            instance.opsForSet().add(USER_REGISTER_REUSE_SHARDING + hashShardingIdx(username), username);
        } finally {
            lock.unlock();
        }
    }

    private <T> String resolveUsername(
            String value,
            Function<String, LambdaQueryWrapper<T>> wrapperSupplier,
            Function<T, String> usernameGetter,
            Function<LambdaQueryWrapper<T>, T> dbQueryFunction) {
        return Optional.ofNullable(dbQueryFunction.apply(wrapperSupplier.apply(value)))
                .map(usernameGetter)
                .orElseThrow(() -> new ClientException("用户名/邮箱/手机号不存在"));
    }

}
